/* Copyright 2010 Anthony Surma
This file is part of Java's Kademlia Extension (JKADX).

JKADX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

JKADX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with JKADX.  If not, see <http://www.gnu.org/licenses/>.
*/

package jkadx.hashCollisions;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import jkadx.networking.NetworkingInterface;
import jkadx.networking.SocketController;
import jkadx.core.CoreConfig;
import jkadx.core.RPC;
import jkadx.dht.Contact;
import jkadx.dht.DHT;
import jkadx.dht.KBucket;
import jkadx.dht.UniqueId;
import jkadx.xml.ElementNode;
/**
 *
 * @author Anthony Surma
 */ 
public class HashCollisionFinder implements NetworkingInterface
{
    private           ArrayList<ElementNode>           settings;
    private         CoreConfig                       coreSettings;
    static final byte   FIRST_CHARACTER_RANGE = 32;//32;
    static final byte   LAST_CHARACTER_RANGE  = 126;//126;
    static final String ALGORITHM             = "SHA1";
           volatile byte[]       successArray = new byte[0];
           String       successResult;
           int          currentIteration      = 0;
    private      SocketController controller;
    private           int                              incomingPort;
    private           int                              outgoingPort;
    volatile byte[] targetHashkey;
    private int targetHashkeyLength;
    final String ONE           = "1";
    final String ZERO          = "0";
    FileInputStream  fis;
    UniqueId myUniqueId;
    final Contact you;
    final int coreIncomingPort;
    private byte[]                        collisionPingHeader;
    private byte[]                        workUnitRequestHeader;
    private byte[]                        collisionWorkUnitHeader;
    private byte[]                        collisionFoundHeader;
    private byte[]                        collisionStopHeader;
    ArrayList<Contact>                    contactsList;
    AtomicBoolean isWorking                     = new AtomicBoolean(false);
    AtomicBoolean isServer                      = new AtomicBoolean(false);
    final Charset  CHARSET     = Charset.forName("UTF-8");
    byte[] startArray;
    final Object startArrayLock = new Object();
    volatile int numIterations;
    int numCores = 1;

    AtomicBoolean first = new AtomicBoolean(true);
    MessageDigest md = MessageDigest.getInstance(ALGORITHM);

    public HashCollisionFinder() throws Exception
    {
        incomingPort = 60000;
        coreSettings      = new CoreConfig("settings", "contacts", -1, -1);
        settings          = coreSettings.getSettings();
        you          = new Contact(myUniqueId = new UniqueId(ElementNode.getDataFor("thisNodeId", settings)),
                       InetAddress.getByName(ElementNode.getDataFor("externalIpAddress", settings)).getAddress(),
                       Integer.parseInt(ElementNode.getDataFor("incomingPort", settings)));
        coreIncomingPort = you.port;

        byte[] bytes           = new byte[RPC.HEADER_LENGTH/* + (UniqueId.NBITS / Byte.SIZE)*/];
        // add my contact information to it (after the flag bits byte)
        you.insert(bytes, RPC.SOURCE_ADDRESS_OFFSET);
        // add the correct flag bits to each rpc
        bytes[0]        = (byte)(RPC.PING_FLAG | RPC.HASHCOLLISIONFINDER);
        collisionPingHeader      = bytes.clone();
        bytes[0]        = (byte)(RPC.FIND_NODE_FLAG | RPC.HASHCOLLISIONFINDER);
        workUnitRequestHeader   = bytes.clone();
        bytes[0]        = (byte)(RPC.PING_FLAG | RPC.FIND_NODE_FLAG | RPC.HASHCOLLISIONFINDER);
        collisionWorkUnitHeader   = bytes.clone();
        bytes[0]        = (byte)(RPC.FIND_VALUE_FLAG | RPC.HASHCOLLISIONFINDER);
        collisionFoundHeader = bytes.clone();
        bytes[0]        = (byte)(RPC.STORE_FLAG | RPC.HASHCOLLISIONFINDER);
        collisionStopHeader   = bytes.clone();
        numCores = Runtime.getRuntime().availableProcessors();

    }


    public void commandLine() throws Exception
    {
        File hashFile;
        Scanner in     = new Scanner(System.in);

        System.out.println("Number of Processors Detected: " + numCores);
        System.out.println("\nHashCollisionFinder Command Line Interface.  Type 'p' to Print menu.");
        while(true)
        {
            String input = null;
            System.out.print("> ");
            input = in.nextLine();
            if (input.equalsIgnoreCase("P"))
            {
                System.out.println("The following commands are not case sensitive:\n");
                System.out.println("(P)rint this menu");
                System.out.println("(G)et contacts list");
                System.out.println("(E)nter ports");
                System.out.println("(L)oad a hash from file ");
                if (!this.isServer.get())
                {
                    System.out.println("(F)ind a hash collision");
                }
                System.out.println("*(A)bout this application");
                if (!this.isServer.get())
                {
                    System.out.println("(S)hutdown");
                }
                else
                {
                    System.out.println("(S)top current job");
                }
                System.out.println();
            }
            else if (input.equalsIgnoreCase("F"))
            {
                if (!this.isServer.get())
                {
                    if (contactsList == null || contactsList.size() == 0)
                    {
                        System.out.println("You must get a list of contacts first.");
                    }
                    else if (this.targetHashkey == null)
                    {
                        System.out.println("You must load a hash from file first.");
                    }
                    else if (this.controller == null)
                    {
                        System.out.println("You must set up your ports first.\n");
                    }
                    else
                    {
                        // set up the job
                        System.out.print("Enter the number of iterations before a worker requests a new chunk: "); // bigger is faster
                        numIterations = (new Scanner(System.in)).nextInt();
                        this.successResult = null;
                        this.successArray = new byte[0];
                        synchronized (startArrayLock)
                        {
                            startArray    = new byte[1];
                            startArray[0] = 32;
                            first.getAndSet(true);
                        }
                        byte[] payload    = new byte[collisionPingHeader.length + targetHashkey.length];

                        for (int i = 0; i < collisionPingHeader.length; i++)
                        {
                            payload[i] = collisionPingHeader[i];
                        }

                        for (int i = collisionPingHeader.length, j = -1; i < payload.length; i++)
                        {
                            payload[i] = targetHashkey[++j];
                        }
                        System.out.print("Would you also like to have this node work on it as well? (Y/n): ");
                        String thisIsWorker = new Scanner(System.in).nextLine();
                        isServer.getAndSet(true);
                        int counter = 0;
                        for (Contact c : contactsList)
                        {
                            ++counter;
                            // send them an application ping
                            controller.sendPacket(payload, c.getInetAddress(), c.port);
                        }
                        System.out.println("Sent " + counter + " recruitment pings to:");
                        for (Contact c : contactsList)
                        {
                            System.out.println(c + "\n");
                        }

                        if (!thisIsWorker.equalsIgnoreCase("n"))
                        {
                            thisIsWorker(payload);
                        }
                    }
                }
            }
            else if (input.equalsIgnoreCase("G"))
            {
                contactsList = loadContacts();
                System.out.println(contactsList.size() + " contacts fetched.\n");
            }
            else if (input.equalsIgnoreCase("E"))
            {
                if (controller == null)
                {
                    coreSettings      = new CoreConfig("settings", "contacts", -1, -1);
                    settings          = coreSettings.getSettings();
                    System.out.print("Enter outgoing port:\n> ");
                    outgoingPort      = in.nextInt();
                    controller = new SocketController(this, incomingPort, outgoingPort);

                }
                System.out.println("Incoming Port: " + incomingPort + "\n");
                System.out.println("Outgoing Port: " + outgoingPort + "\n");
            }
            else if (input.equalsIgnoreCase("S"))
            {
                if (!this.isServer.get())
                {
                    controller.closeSockets();
                    System.out.println("Shutting down sockets and exiting...");
                    return;
                }
                else
                {
                    int counter = 0;
                    for (Contact c : contactsList)
                    {
                        try
                        {
                            ++counter;
                            controller.sendPacket(this.collisionStopHeader, c.getInetAddress(), c.port);
                        } catch (IOException ex) {}
                    }
                    this.isServer.getAndSet(false);
                    System.out.println("Sent stop requests to " + counter + " nodes.");
                }
            }
            else if (input.equalsIgnoreCase("L"))
            {
                hashFile = new File("hash.key");
                if (hashFile.exists())
                {
                    targetHashkey = new byte[20];
                    fis = new FileInputStream(hashFile);
                    fis.read(targetHashkey);
                    fis.close();
                    StringBuilder sb = new StringBuilder(20);
                    for (byte b : targetHashkey)
                    {
                        for (int i = 0; i < Byte.SIZE; ++i)
                        {
                            if (((b << i) & 0x80) == 0x80)
                            {
                                sb.append(ONE);
                            }
                            else
                            {
                                sb.append(ZERO);
                            }
                        }
                    }
                    System.out.println("hash.key properties:\n" +
                            "Number of Bytes: " + hashFile.length() + "\n" +
                            "Contents: " + sb.toString() + "\n");
                }
                else
                {
                    System.out.println("Cannot find file: hash.key");
                }
                System.out.println();
            }
        }
    }

    void thisIsWorker(byte[] payload)
    {
        System.out.println("Experimental: Sending a work ping to yourself.");
        try
        {
            // send thisnode a workrequest
            controller.sendPacket(payload, you.getInetAddress(), you.port);
        } catch (IOException ex) {}
    }

    ArrayList<Contact> loadContacts()
    {

        ArrayList<Contact> result = new ArrayList<Contact>();
        try
	{
		final FileInputStream   fis          = new FileInputStream("contacts");
		final ObjectInputStream ois          = new ObjectInputStream(fis);
		final String            akb          = ois.readUTF();
		final String[]          kbuckets;
		final int               id           = 0;
		final int               address      = 1;
		final int               port         = 2;

                      String[]          contacts;

		ois.close();
		fis.close();
                kbuckets = akb.split(DHT.SEPERATOR);
		for (String k : kbuckets)
		{
		    contacts = k.split(KBucket.SEPERATOR);
                    for (String c : contacts)
                    {
                        //System.out.println(c);
                        String[]  s = c.split(Contact.SEPERATOR);
                        Contact old = new Contact(new UniqueId(s[id]),
                                      InetAddress.getByName(s[address]).getAddress(),
                                      Integer.parseInt(s[port]));

                       // dht.updateOrAddNew(old);
                        result.add(old);
                    }
		}
                return result;
        }
	catch (FileNotFoundException ex) {return result;}
	catch (IOException ex) {return result;}
    }


    public static void main (String[] args) throws Exception
    {

       
       
        HashCollisionFinder hcf    = new HashCollisionFinder();
              
        hcf.commandLine();
        return;
       
    }
    
    public boolean newRound(byte[] startingArray, int maxIterations, byte[] targetHashkey, MessageDigest md)
    {        
        byte[] workingArray = new byte[0];
        byte[] workingHashkey;
        int currentIteration = 0;
                
        do
        {
            boolean done;

            if (currentIteration == 0)
            {
                workingArray = startingArray;
                System.out.print("Starting array: ");
                for (byte b : workingArray)
                {
                    System.out.print(b + " ");
                }
                System.out.println();
                currentIteration = 1;
            }
            else
            {
                workingArray = new byte[workingArray.length + 1];
                System.out.println("Current array size: " + workingArray.length);
                Arrays.fill(workingArray, FIRST_CHARACTER_RANGE);
            }

            do
            {
                done = true;

                while ((workingArray[0] <= LAST_CHARACTER_RANGE) && (currentIteration <= maxIterations))
                {                  
                    workingHashkey = md.digest(workingArray);
                    if (Arrays.equals(workingHashkey, targetHashkey))
                    {
                        successArray = workingArray;                        
                        return true;
                    }
                    ++workingArray[0];
                    ++currentIteration;
                }

                for (int i = 0; i < workingArray.length ; i++)
                {
                    if (workingArray[i] < LAST_CHARACTER_RANGE)
                    {
                        ++workingArray[i];
                        done = false;
                        break;
                    }
                    else
                    {
                        workingArray[i] = FIRST_CHARACTER_RANGE;
                    }
                }
            } while (!done && isWorking.get());
        } while ((currentIteration <= maxIterations) && (isWorking.get()));
        
        return false; // maxIteration passed without success
    }

    public static byte[] getNextStartingArray(byte[] lastStartingArray, int numIterations)
    {
        //byte[] result = null;
        int currentIteration = 0;

        do
        {
            boolean done;

            if (currentIteration == 0)
            {               
                currentIteration = 1;
            }
            else
            {
                lastStartingArray = new byte[lastStartingArray.length + 1];               
                Arrays.fill(lastStartingArray, FIRST_CHARACTER_RANGE);
            }

            do
            {
                done = true;

                while ((lastStartingArray[0] <= LAST_CHARACTER_RANGE) && (currentIteration <= numIterations))
                {                  
                    ++lastStartingArray[0];
                    ++currentIteration;
                }

                if (currentIteration < numIterations)
                {
                    for (int i = 0; i < lastStartingArray.length; i++)
                    {
                        if (lastStartingArray[i] < LAST_CHARACTER_RANGE)
                        {
                            ++lastStartingArray[i];
                            done = false;
                            break;
                        }
                        else
                        {
                            lastStartingArray[i] = FIRST_CHARACTER_RANGE;
                        }
                    }
                }
            } while (!done);
        } while (currentIteration <= numIterations);

        return lastStartingArray;
    }


    public static char[] byteArrayToCharArray(byte[] byteArray)
    {
        char[] result = new char[byteArray.length];

        for (int j = 0; j < byteArray.length; j++)
        {
            result[j] = (char)byteArray[j];
        }

        return result;

    }

    public void processIncoming(DatagramPacket packet)
    {
       byte[] payload = packet.getData();
       switch (payload[0] & 0x0F)
       {
           case 1: // received ping
           {
               System.out.println("received Request Ping (" + packet.getLength() + " bytes)");
               receivedPing(payload, packet.getLength());
               break;
           }
           case 2: // received workUnitRequest
           {
               System.out.println("received workUnitRequest (" + packet.getLength() + " bytes)");
               receivedWorkUnitRequest(payload);
               break;
           }
           case 3: // received workUnit
           {
               System.out.println("received workUnit (" + packet.getLength() + " bytes)");
               receivedWorkUnit(payload, packet.getLength());
               break;
           }
           case 4: // received collision found
           {
               System.out.println("received collision found (" + packet.getLength() + " bytes)");
               receivedCollisionFound(payload, packet.getLength());
               break;
           }
           case 8: // recieved stop
           {
               System.out.println("recieved stop (" + packet.getLength() + " bytes)");
               receivedStop(payload);
               break;
           }
           default:
               return;
       }
    }
    
    void receivedPing(byte[] payload, int length)
    {
        if (isWorking.compareAndSet(false, true))
        {
            // atleast get length of targetHashkey
            this.targetHashkeyLength = length - RPC.HEADER_LENGTH;
            System.out.println("Target hashkey length = " + this.targetHashkeyLength);
            // send workunit request
            Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);
                     
            try
            {
                System.out.println("Sending initial work unit request to: " + c.getInetAddress() + "/" + c.port);
                controller.sendPacket(this.workUnitRequestHeader, c.getInetAddress(), c.port);
            } catch (IOException ex) {}
        }
    }
    
    void receivedWorkUnitRequest(byte[] payload)
    {
        if (isServer.get())
        {
            // get nextStartingArray
            byte[] nextStartingArray;

            if (first.compareAndSet(true, false))
            {
                nextStartingArray = new byte[1];
                nextStartingArray[0] = 32;
            }
            else
            {
                synchronized (startArrayLock)
                {
                    nextStartingArray = startArray = HashCollisionFinder.getNextStartingArray(startArray.clone(), numIterations);
                }
            }

            Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);

            // send header + maxIterations + target hash + nextstartingarray
            payload = new byte[RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE) +
                      targetHashkey.length + nextStartingArray.length];

            for (int i = 0; i < RPC.HEADER_LENGTH; ++i)
            {
                payload[i] = this.collisionWorkUnitHeader[i];
            }

            payload[RPC.HEADER_LENGTH]     |= numIterations >>> 24;
            payload[RPC.HEADER_LENGTH + 1] |= numIterations >>> 16;
            payload[RPC.HEADER_LENGTH + 2] |= numIterations >>> 8;
            payload[RPC.HEADER_LENGTH + 3] |= numIterations;

            for (int i = RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE), j = -1; i < RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE) + targetHashkey.length; i++)
            {
                payload[i] = targetHashkey[++j];
            }

            for (int i = RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE) + targetHashkey.length, j = -1; i < payload.length; ++i)
            {
                payload[i] = nextStartingArray[++j];
            }

            // send workunit
            System.out.println("Sent workunit packet (" + payload.length + " bytes)");
            System.out.print("Starting array contents: ");
            for (byte b : nextStartingArray)
            {
                System.out.print(b + " ");
            }
            try
            {
                controller.sendPacket(payload, c.getInetAddress(), c.port);
            } catch (IOException ex) {}
        }
    }
    
    void receivedWorkUnit(byte[] payload, int length)
    {
        if (isWorking.get())
        {
            byte[] targetHashkey = new byte[targetHashkeyLength];
            byte[] startingArray = new byte[length - RPC.HEADER_LENGTH - (Integer.SIZE / Byte.SIZE) - targetHashkey.length];


            int maxIterations = ((payload[RPC.HEADER_LENGTH    ] & 0xFF) << 24) +
                                ((payload[RPC.HEADER_LENGTH + 1] & 0xFF) << 16) +
                                ((payload[RPC.HEADER_LENGTH + 2] & 0xFF) << 8 ) +
                                 (payload[RPC.HEADER_LENGTH + 3] & 0xFF);

            
            // get target hash
            for (int i = RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE), j = -1; i < RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE) + targetHashkey.length; i++)
            {
                targetHashkey[++j] = payload[i];
            }

            for (int i = RPC.HEADER_LENGTH + (Integer.SIZE / Byte.SIZE) + targetHashkey.length, j = -1; i < length; i++)
            {
                startingArray[++j] = payload[i];
            }

            // start working on it

            // send workunit request or collision found when done,
            // unless told to stop
            System.out.print("Targethashkey: ");
            StringBuilder sb = new StringBuilder(20);
            for (byte b : targetHashkey)
            {
                for (int i = 0; i < Byte.SIZE; ++i)
                {                
                    if (((b << i) & 0x80) == 0x80) // this is definately the correct way
                    {
                        sb.append(ONE);
                    }
                    else
                    {
                        sb.append(ZERO);
                    }                
                }
            }
            System.out.println(sb.toString());
            System.out.println("Targethashkey length: " + targetHashkey.length);
            System.out.println("StartingArray length: " + startingArray.length);
            System.out.println("Max Iterations      : " + maxIterations);
            boolean result = newRound(startingArray, maxIterations, targetHashkey, md);
            if (isWorking.get())
            {
                Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);
                if (result)
                {
                    payload = new byte[RPC.HEADER_LENGTH + this.successArray.length];

                    for (int i = 0; i < RPC.HEADER_LENGTH; i++)
                    {
                        payload[i] = this.collisionFoundHeader[i];
                    }

                    for (int i = RPC.HEADER_LENGTH, j = -1; i < payload.length; i++)
                    {
                        payload[i] = this.successArray[++j];
                    }
                    //send notification of collision
                    try
                    {
                        controller.sendPacket(payload, c.getInetAddress(), c.port);

                    } catch (IOException ex) {}
                }
                else
                {
                    c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);

                    try
                    {
                        controller.sendPacket(this.workUnitRequestHeader, c.getInetAddress(), c.port);
                    } catch (IOException ex) {}
                }
            }
        }
    }
    
    void receivedCollisionFound(byte[] payload, int length)
    {
        if (isServer.get())
        {
            // verify results
            byte[] data = new byte[length - RPC.HEADER_LENGTH];

            for (int i = RPC.HEADER_LENGTH, j = -1; i < length; i++)
            {
                data[++j] = payload[i];
            }
            String result = new String(this.byteArrayToCharArray(data));
            System.out.println("Receied potential collision: " + result);
            byte[] hash = null;
            try
            {
                hash = MessageDigest.getInstance(ALGORITHM).digest(result.getBytes(CHARSET));
            } catch (NoSuchAlgorithmException ex) {}
            if (Arrays.equals(hash, targetHashkey))
            {
                this.isWorking.getAndSet(false); // experimental

                this.isServer.getAndSet(false);
                this.successArray  = data;
                this.successResult = result;
                System.out.println("Verified collision! Done.");
                System.out.println("Sending Stop request to all nodes.");

                for (Contact c : contactsList)
                {
                    try
                    {
                        controller.sendPacket(this.collisionStopHeader, c.getInetAddress(), c.port);
                    } catch (IOException ex) {}
                }
            }
            else
            {
                System.out.println("Received bogus results.");
            }
        }
    }
    
    void receivedStop(byte[] payload)
    {
        if (isWorking.get())
        {
            isWorking.getAndSet(false);
            // stop working
        }
    }

}
